#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#define strncasecmp _strnicmp
#endif

#include "FreeImage.h"
#include "gip.h"

int gip_init() {
    FreeImage_Initialise( false);
    return 0;
}

int gip_deInit() {
    FreeImage_DeInitialise();
    return 0;
}


#define FiFormat FREE_IMAGE_FORMAT
#define FiB(image) FIBITMAP* fb = (FIBITMAP*)(image);

static FiFormat _toFiFormat_[] = {
    FIF_BMP,
    FIF_DDS,
    FIF_JPEG,
    FIF_PNG,
    FIF_TARGA
};

static FiFormat filenameToFiFormat( const char* filename) {
    const char* suffix;
    if ( strlen( filename) < 4)
        return FIF_UNKNOWN;
    suffix = filename + strlen( filename) - 3;
    if ( strncasecmp( suffix, "bmp", 3) == 0)
        return FIF_BMP;
    else if ( strncasecmp( suffix, "dds", 3) == 0)
        return FIF_DDS;
    else if ( strncasecmp( suffix, "jpg", 3) == 0)
        return FIF_JPEG;
    else if ( strncasecmp( suffix, "png", 3) == 0)
        return FIF_PNG;
    else if ( strncasecmp( suffix, "tga", 3) == 0)
        return FIF_TARGA;
    else
        return FIF_UNKNOWN;
}

int gip_loadImageFromFile( const char* filename, void** image) {
    FIBITMAP* fi;
    FiFormat fif = FreeImage_GetFileType( filename, 0);
    if ( fif == FIF_UNKNOWN)
        return -1;
    
    fi = FreeImage_Load( fif, filename, 0);
    if ( fi) {
        *image = fi;
        return 0;
    }

    return -1;
}

int gip_loadImageFromMemory( const void* data, size_t size, void** image) {
    FIBITMAP* fi;
    FiFormat fif;
    FIMEMORY* fm = FreeImage_OpenMemory( (BYTE*)data, size);
    if ( !fm)
        return -1;
    fif = FreeImage_GetFileTypeFromMemory( fm, size);
    if ( fif == FIF_UNKNOWN) {
        FreeImage_CloseMemory( fm);
        return -1;
    }

    fi = FreeImage_LoadFromMemory( fif, fm, 0);
    FreeImage_CloseMemory( fm);
    if ( fi) {
        *image = fi;
        return 0;
    }
    return -1;
}

void gip_freeImage( void* image) {
    FiB( image);
    FreeImage_Unload( fb);
}

int gip_getImageW( void* image) {
    FiB( image);
    return (int)FreeImage_GetWidth( fb);
}

int gip_getImageH( void* image) {
    FiB( image);
    return (int)FreeImage_GetHeight( fb);
}

static int _jpeg_save_qulity_ = JPEG_QUALITYGOOD;
void gip_setJpgSaveQuality( int percent) {
    /*if ( percent <= 15)
        _jpeg_save_qulity_ = JPEG_QUALITYBAD;
    else if ( percent <= 35)
        _jpeg_save_qulity_ = JPEG_QUALITYAVERAGE;
    else if ( percent <= 60)
        _jpeg_save_qulity_ = JPEG_QUALITYNORMAL;
    else if ( percent <= 90)
        _jpeg_save_qulity_ = JPEG_QUALITYGOOD;
    else
       _jpeg_save_qulity_ = JPEG_QUALITYSUPERB;  
    */
    if ( percent < 15)
    	percent = 15;
    else if ( percent > 100)
    	percent = 100;
    _jpeg_save_qulity_ = percent & 0x7F;
}

int gip_createParentDir( const char* filename) {
    const char* p = filename + strlen(filename) - 1;
    int i = 0;
    for ( ;p != filename; ++i, --p) {
        if ( *p == '/' || *p == '\\') {
            char command[1024];
#if defined(_WIN32) || defined(_WIN64)
            _snprintf( command, 1024, "mkdir %s", filename);
            command[strlen(command)-1-i] = '\0';
            _snprintf( command, 1024, "%s 2>tmp.txt", command);
#else
            snprintf( command, 1024, "mkdir -p %s", filename);
            command[strlen(command)-1-i] = '\0';
#endif
            system( command);
            break;
        }
    }
    return 0;
}

int splitChannel( void* image, FIMEMORY** rgbBuffer, FIMEMORY** alphaBuffer) {
    FiB( image);
    FIBITMAP* biRgb = NULL;
    FIBITMAP* biAlpha = NULL;
    FIBITMAP* biAlpha32 = NULL;
    FIMEMORY* fmRgb = NULL;
    FIMEMORY* fmAlpha = NULL;
    int ret = 0;
    int flag = 0;

    biAlpha = FreeImage_GetChannel( fb, FICC_ALPHA);
    if ( !biAlpha)
        return -1;
    biRgb = FreeImage_ConvertTo24Bits( fb); 
    if ( !biRgb) {
        ret = -2;
        goto GET_END;
    }
    flag |= _jpeg_save_qulity_;
    fmRgb = FreeImage_OpenMemory( 0, 0);
    if ( !FreeImage_SaveToMemory( FIF_JPEG, biRgb, fmRgb, flag)) {
        printf( "save failed\n");
        ret = -3;
        goto GET_END;
    }

    biAlpha32 = FreeImage_Allocate( FreeImage_GetWidth( fb),
            FreeImage_GetHeight( fb), 32);
    FreeImage_SetChannel( biAlpha32, biAlpha, FICC_ALPHA); 

    fmAlpha = FreeImage_OpenMemory( 0, 0);
    if ( !FreeImage_SaveToMemory( FIF_PNG, biAlpha32, fmAlpha, flag)) {
        printf( "save alpha failed\n");
        ret = -3;
        goto GET_END;
    }
GET_END:
    if ( biRgb)
        FreeImage_Unload( biRgb);
    if ( biAlpha)
        FreeImage_Unload( biAlpha);
    if ( biAlpha32)
        FreeImage_Unload( biAlpha32);
    if ( ret) {
        if ( fmRgb)
            FreeImage_CloseMemory( fmRgb);
        if ( fmAlpha)
            FreeImage_CloseMemory( fmAlpha);
        return ret;
    }
    *rgbBuffer = fmRgb;
    *alphaBuffer = fmAlpha;
    return 0;
}

typedef struct ChunkInfo
{
    unsigned int offset;
    unsigned int size;
    int flag;
} ChunkInfo;

typedef struct JaiHeader
{
    char        magic[4];
    int         chunkNum;
} JaiHeader;

int saveJaiFile( void* image, const char* filename) {
    FIMEMORY* fmRgb = NULL;
    FIMEMORY* fmAlpha = NULL;
    BYTE* bufRgb = NULL;
    BYTE* bufAlpha = NULL;
    ChunkInfo infoRgb;
    ChunkInfo infoAlpha;
    int ret = 0;

    if ( splitChannel( image, &fmRgb, &fmAlpha))
        return -1;
    
    FILE* file = fopen( filename, "wb");
    if ( !file) {
        ret = -2;
        goto END;
    }

    JaiHeader header;
    strcpy( header.magic, "jai");
    header.chunkNum = 2;
    if ( fwrite( &header, 1, sizeof(header), file) != sizeof(header)) {
        ret = -3;
        goto END;
    }

    {
        DWORD dwSize;
        if ( !FreeImage_AcquireMemory( fmRgb, &bufRgb, &dwSize)) {
            ret = -4;
            goto END;
        }
        infoRgb.size = (unsigned int)dwSize;
    }
    {
        DWORD dwSize;
        if ( !FreeImage_AcquireMemory( fmAlpha, &bufAlpha, &dwSize)) {
            ret = -4;
            goto END;
        }
        infoAlpha.size = (unsigned int)dwSize;
    }

    infoRgb.offset = sizeof( header) + sizeof( ChunkInfo) * 2;
    infoRgb.flag = 1;
    if ( fwrite( &infoRgb, 1, sizeof( infoRgb), file) != sizeof( infoRgb)) {
        ret = -5;
        goto END;
    }
    
    infoAlpha.offset = infoRgb.offset + infoRgb.size;
    infoAlpha.flag = 0;
    if ( fwrite( &infoAlpha, 1, sizeof( infoAlpha), file) != sizeof( infoAlpha)) {
        ret = -5;
        goto END;
    }

    if ( fwrite( bufRgb, 1, infoRgb.size, file) != infoRgb.size) {
        ret = -6;
        goto END;
    }
    if ( fwrite( bufAlpha, 1, infoAlpha.size, file) != infoAlpha.size) {
        ret = -6;
        goto END;
    }

END:
    FreeImage_CloseMemory( fmRgb);
    FreeImage_CloseMemory( fmAlpha);
    if ( file)
        fclose( file);
    return ret;

}

static int saveJaiMemory( void* image, void** data, size_t* size) {
    FIMEMORY* fmRgb = NULL;
    FIMEMORY* fmAlpha = NULL;
    BYTE* bufRgb = NULL;
    BYTE* bufAlpha = NULL;
    DWORD rgbSize = 0;
    DWORD alphaSize = 0;
    char* dstBuffer = NULL;
    int ret = 0;

    if ( splitChannel( image, &fmRgb, &fmAlpha))
        return -1;

    if ( !FreeImage_AcquireMemory( fmRgb, &bufRgb, &rgbSize)) {
        ret = -4;
        goto END;
    }
    if ( !FreeImage_AcquireMemory( fmAlpha, &bufAlpha, &alphaSize)) {
        ret = -4;
        goto END;
    }

    *size = sizeof(DWORD) * 2 + rgbSize + alphaSize;
    dstBuffer = (char*)malloc(*size); 
    memcpy( dstBuffer, &rgbSize, sizeof(DWORD));
    memcpy( dstBuffer + sizeof(DWORD), bufRgb, rgbSize);
    memcpy( dstBuffer + sizeof(DWORD) + rgbSize, &alphaSize, 4);
    memcpy( dstBuffer + sizeof(DWORD) * 2 + rgbSize, bufAlpha, alphaSize);

END:
    FreeImage_CloseMemory( fmRgb);
    FreeImage_CloseMemory( fmAlpha);

    if ( dstBuffer) {
        *data = dstBuffer;
        return 0;
    } else {
        return -1;
    }
}

int gip_saveToFile( void* image, int format, const char* filename) {
    if ( gip_createParentDir( filename))
        return -2;

    if ( format == gip_JAI)
        return saveJaiFile( image, filename);

    FiFormat fif;
    FiB( image);

    if ( format < 0 || format >= sizeof( _toFiFormat_) / sizeof( FiFormat))
        return -1;
    fif = _toFiFormat_[format];

    FIBITMAP* fb2 = fb;
    int flag = 0;
    if ( format == gip_JPG) {
        flag |= _jpeg_save_qulity_;
        fb2 = FreeImage_ConvertTo24Bits( fb);
        if ( !fb2)
            return -4;
    }
    int ret = FreeImage_Save( fif, fb2, filename, flag) ? 0 : -2;
    if ( fb2 != fb)
        FreeImage_Unload( fb2);
    return ret;
}

int gip_saveToMemory( void* image, int format, void** data, size_t* size, void** extra) {
    FiB( image);
    FIMEMORY* fm;
    FiFormat fif;

    if ( format == gip_JAI) {
        *extra = NULL;
        return saveJaiMemory( image, data, size);
    }

    if ( format < 0 || format >= sizeof( _toFiFormat_) / sizeof( FiFormat))
        return -1;
    fif = _toFiFormat_[format];
    FIBITMAP* fb2 = fb;
    int flag = 0;
    if ( format == gip_JPG) {
        flag |= _jpeg_save_qulity_;
        fb2 = FreeImage_ConvertTo24Bits( fb);
        if ( !fb2)
            return -4;
    }
    fm = FreeImage_OpenMemory( 0, 0);
    if ( !FreeImage_SaveToMemory( fif, fb2, fm, flag)) {
        if ( fb2 != fb)
            FreeImage_Unload( fb2);
        return -2;
    }
    
    {
        DWORD dwSize;
        BYTE* byData;
        if ( !FreeImage_AcquireMemory( fm, &byData, &dwSize)) {
            FreeImage_CloseMemory( fm); {
                    if ( fb2 != fb)
                        FreeImage_Unload( fb2);
                return -3;
            }
        }
        *data = (void*)byData;
        *size = (size_t)dwSize;
        *extra = fm;
    }

    if ( fb2 != fb)
        FreeImage_Unload( fb2);
    return 0;
}

void gip_freeMemory( void* data, void* extra) {
    FIMEMORY* fm = (FIMEMORY*)extra;
    if ( fm)
        FreeImage_CloseMemory( fm);
    else if ( data)
        free( data);
}

#define getPixel( pixels, r, c, pitch) (pixels + r * pitch + (c << 2))

int gip_validArea( void* image, int* x, int* y, int* w, int* h) {
    FiB( image);
    int imageW = (int)FreeImage_GetWidth( fb);
    int imageH = (int)FreeImage_GetHeight( fb);
    BYTE* pixels = FreeImage_GetBits( fb);
    const int bypp = 4;
    unsigned int pitch = FreeImage_GetPitch( fb);
    FREE_IMAGE_TYPE fit = FreeImage_GetImageType( fb);

    int left = 0;
    int top = 0;
    int right = imageW - 1;
    int bottom = imageH - 1;
    int r = 0;
    int c = 0;
    BYTE* pixel;

    if ( fit != FIT_BITMAP)
        return -1;
    if ( FreeImage_GetBPP( fb) != 32) {
        *x = 0;
        *y = 0;
        *w = imageW;
        *h = imageH;
        return 0;
    }

    for ( r = 0; r < imageH; ++r) {
        for ( c = 0; c < imageW; ++c) {
            pixel = getPixel( pixels, r, c, pitch);
            if ( pixel[FI_RGBA_ALPHA] != 0) {
                top = r;
                left = c;
                goto FIND_LEFT;
            }
        }
    }
    goto UN_FINDED;

FIND_LEFT:
    for ( c = 0; c < left; ++c) {
        for ( r = top; r < imageH; ++r) {
            pixel = getPixel( pixels, r, c, pitch);
            if ( pixel[FI_RGBA_ALPHA] != 0) {
                left = c;
                goto FIND_BOTTOM;
            }
        }
    }

FIND_BOTTOM:
    for ( r = imageH - 1; r >= top; --r) {
        for ( c = imageW - 1; c >= left; --c) {
            pixel = getPixel( pixels, r, c, pitch);
           if ( pixel[FI_RGBA_ALPHA] != 0) {
               bottom = r;
               right = c;
               goto FIND_RIGHT;
           }
        }
    }
    goto END;

FIND_RIGHT:
    for ( c = imageW - 1; c >= right; --c) {
        for ( r = bottom; r > top; --r) {
            pixel = getPixel( pixels, r, c, pitch);
           if ( pixel[FI_RGBA_ALPHA] != 0) {
               right = c;
               goto END;
           }
        }
    }

END:
    *x = left;
    *y = imageH - 1 - bottom;
    *w = right - left + 1;
    *h = bottom - top + 1;
    
    return 0;
    
UN_FINDED:
    *x = imageW - 1;
    *y = imageH - 1;
    *w = 0;
    *h = 0;
    return 0;
    
}

void* gip_cutImage( void* image, int x, int y, int w, int h) {
    FiB( image);
    FIBITMAP* newFb;
    int bpp = FreeImage_GetBPP( fb);
    if ( bpp < 4 || w <= 0 || h <= 0)
        return NULL;
    newFb = FreeImage_Allocate( w, h, bpp, 0, 0, 0);
    if ( !newFb)
        return NULL;
    
    y = FreeImage_GetHeight( fb) - 1 - y;
    if ( y < 0 || x < 0)
        return NULL;        
    {
        int oldPitch = FreeImage_GetPitch( fb);
        const BYTE* oldBits = FreeImage_GetBits( fb);
        int newPitch = FreeImage_GetPitch( newFb);
        BYTE* newBits = FreeImage_GetBits( newFb) + newPitch * (h - 1);
        size_t lineSize = w * (bpp / 8);
        int r = 0;

        oldBits += ( y * oldPitch + x * (bpp / 8));
        for ( r = 0; r < h; ++r) {
            memcpy( newBits, oldBits, lineSize);
            oldBits -= oldPitch;
            newBits -= newPitch;
        }
    }

    return newFb;
}

#if defined(_WIN32) || defined(_WIN64)
#include <io.h>

int gip_transDir( const char* path, gip_transDirFunc callBack, void* data, int transSub) {
    char fullPath[4096];
    char currFile[4096];
    long hFile;
    _finddata_t fileinfo;
    strcpy( fullPath, path);
    strcat( fullPath, "\\*.*");
    hFile = _findfirst( fullPath, &fileinfo);
    if ( hFile == -1)
        return -1;
    
    do {
        if ( strcmp( fileinfo.name, ".") == 0 ||
            strcmp( fileinfo.name, "..") == 0)
            continue;

        sprintf( currFile, "%s\\%s", path, fileinfo.name);

        if ( fileinfo.attrib & _A_SUBDIR) {
            callBack( currFile, 0, data);
            if ( transSub)
                gip_transDir( currFile, callBack, data, transSub); 
        }
        else
            callBack( currFile, 1, data);
    }while ( _findnext( hFile, &fileinfo) == 0);

    _findclose( hFile);

    return 0;
}

#else
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>

int gip_transDir( const char* path, gip_transDirFunc callBack, void* data, int transSub) {
    DIR* dir;
    char fullPath[1024], currFile[1024];
    struct dirent* de;
    struct stat fs;
    strcpy( fullPath, path);
    if ( path[strlen(path) -1] != '/')
        strcat( fullPath, "/");

    dir = opendir( fullPath);
    if ( !dir)
        return -1;
    
    while ( (de = readdir( dir)) != NULL) {
        if ( strcmp( de->d_name, ".") == 0 ||
             strcmp( de->d_name, "..") == 0)
            continue;

        sprintf( currFile, "%s%s", fullPath, de->d_name);
        stat( currFile, &fs);
        if ( S_ISDIR( fs.st_mode)) {
            callBack( currFile, 0, data);
            if ( transSub)
                gip_transDir( currFile, callBack, data, transSub);
        }
        else
            callBack( currFile, 1, data);
    }

    closedir( dir);

    return 0;
}
#endif
